#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2023, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #

ONE_LOCATION = ENV['ONE_LOCATION']

if !ONE_LOCATION
    RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
    GEMS_LOCATION     = '/usr/share/one/gems'
else
    RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
    GEMS_LOCATION     = ONE_LOCATION + '/share/gems'
end

# %%RUBYGEMS_SETUP_BEGIN%%
if File.directory?(GEMS_LOCATION)
    real_gems_path = File.realpath(GEMS_LOCATION)
    if !defined?(Gem) || Gem.path != [real_gems_path]
        $LOAD_PATH.reject! {|l| l =~ /vendor_ruby/ }

        # Suppress warnings from Rubygems
        # https://github.com/OpenNebula/one/issues/5379
        begin
            verb = $VERBOSE
            $VERBOSE = nil
            require 'rubygems'
            Gem.use_paths(real_gems_path)
        ensure
            $VERBOSE = verb
        end
    end
end
# %%RUBYGEMS_SETUP_END%%

$LOAD_PATH << RUBY_LIB_LOCATION
$LOAD_PATH << RUBY_LIB_LOCATION + '/cli'

require 'command_parser'
require 'one_helper/onevnet_helper'
require 'one_helper/onecluster_helper'

CommandParser::CmdParser.new(ARGV) do
    usage '`onevnet` <command> [<args>] [<options>]'
    version OpenNebulaHelper::ONE_VERSION

    helper = OneVNetHelper.new

    before_proc do
        helper.set_client(options)
    end

    USE = {
        :name => 'use',
        :large => '--use',
        :description => 'lock use actions'
    }

    MANAGE = {
        :name => 'manage',
        :large => '--manage',
        :description => 'lock manage actions'
    }

    ADMIN = {
        :name => 'admin',
        :large => '--admin',
        :description => 'lock admin actions'
    }

    ALL = {
        :name => 'all',
        :large => '--all',
        :description => 'lock all actions'
    }

    FORCE = {
        :name => 'force',
        :large => '--force',
        :description => 'Force execution of action, ' \
                        'bypass the consistency checks'
    }

    SUCCESS = {
        :name => 'success',
        :large => '--success',
        :description => 'Recover a Virtual Network by succeeding ' \
                        'the pending action'
    }

    FAILURE = {
        :name => 'failure',
        :large => '--failure',
        :description => 'Recover a Virtual Network by failing ' \
                        'the pending action'
    }

    DELETE = {
        :name => 'delete',
        :large => '--delete',
        :description => 'No recover action possible, delete the Virtual Network'
    }

    RETRY = {
        :name => 'retry',
        :large => '--retry',
        :description => 'Recover a Virtual Network by retrying the last ' \
                        'failed action'
    }

    ########################################################################
    # Global Options
    ########################################################################
    set :option, CommandParser::OPTIONS + OpenNebulaHelper::CLIENT_OPTIONS

    CREATE_OPTIONS = [OneClusterHelper::CLUSTER]
    STD_OPTIONS = CommandParser::OPTIONS + OpenNebulaHelper::CLIENT_OPTIONS

    ########################################################################
    # Formatters for arguments
    ########################################################################
    set :format, :groupid, OpenNebulaHelper.rname_to_id_desc('GROUP') do |arg|
        OpenNebulaHelper.rname_to_id(arg, 'GROUP')
    end

    set :format, :userid, OpenNebulaHelper.rname_to_id_desc('USER') do |arg|
        OpenNebulaHelper.rname_to_id(arg, 'USER')
    end

    set :format, :vnetid, OneVNetHelper.to_id_desc do |arg|
        helper.to_id(arg)
    end

    set :format, :vnetid_list, OneVNetHelper.list_to_id_desc do |arg|
        helper.list_to_id(arg)
    end

    set :format, :filterflag, OneVNetHelper.filterflag_to_i_desc do |arg|
        helper.filterflag_to_i(arg)
    end

    set :format, :ar_id, 'Integer' do |arg|
        format_int(arg)
    end

    ########################################################################
    # Commands
    ########################################################################

    create_desc = <<-EOT.unindent
        Creates a new Virtual Network from the given template

        #{OpenNebulaHelper::TEMPLATE_INPUT}
    EOT

    command :create, create_desc, [:file, nil], :options => CREATE_OPTIONS do
        cid = options[:cluster] || ClusterPool::NONE_CLUSTER_ID

        helper.create_resource(options) do |vn|
            begin
                if args[0]
                    template = File.read(args[0])
                elsif !(stdin = OpenNebulaHelper.read_stdin).empty?
                    template = stdin
                end
                vn.allocate(template, cid)
            rescue StandardError => e
                STDERR.puts "Error creating network: #{e.message}"
                exit(-1)
            end
        end
    end

    delete_desc = <<-EOT.unindent
        Deletes the given Virtual Network
    EOT

    command :delete, delete_desc, [:range, :vnetid_list] do
        helper.perform_actions(args[0], options, 'deleting') do |vn|
            vn.delete
        end
    end

    addar_desc = <<-EOT.unindent
        Adds an address range to the Virtual Network
    EOT

    command :addar, addar_desc, :vnetid, [:file, nil],
            :options => STD_OPTIONS + OneVNetHelper::ADDAR_OPTIONS do
        helper.perform_action(args[0], options, 'address range added') do |vn|
            if args[1]
                ar = File.read(args[1])
            elsif !(stdin = OpenNebulaHelper.read_stdin).empty?
                ar = stdin
            else
                ar = OpenNebulaHelper.create_ar(options)
            end

            vn.add_ar(ar)
        end
    end

    addleases_desc = <<-EOT.unindent
        (DEPRECATED, use addar) Adds a lease to the Virtual Network
    EOT

    command :addleases, addleases_desc, :vnetid, :ip, [:mac, nil] do
        helper.perform_action(args[0], options, 'lease added') do |vn|
            vn.addleases(args[1], args[2])
        end
    end

    rmar_desc = <<-EOT.unindent
        Removes an address range from the Virtual Network
    EOT

    command :rmar, rmar_desc, :vnetid, :ar_id, :options => FORCE do
        helper.perform_action(args[0], options, 'address range removed') do |vn|
            vn.rm_ar(args[1], options[:force] || false)
        end
    end

    rmleases_desc = <<-EOT.unindent
        (DEPRECATED, use rmar) Removes a lease from the Virtual Network
    EOT

    command :rmleases, rmleases_desc, :vnetid, :ip do
        helper.perform_action(args[0], options, 'lease removed') do |vn|
            vn.rmleases(args[1])
        end
    end

    free_desc = <<-EOT.unindent
        Frees a reserved address range from the Virtual Network
    EOT

    command :free, free_desc, :vnetid, :ar_id do
        helper.perform_action(args[0], options, 'address range freed') do |vn|
            vn.free(args[1])
        end
    end

    hold_desc = <<-EOT.unindent
        Holds a Virtual Network lease, marking it as used
    EOT

    command :hold, hold_desc, :vnetid, :ip,
            :options => STD_OPTIONS + [OneVNetHelper::AR] do
        helper.perform_action(args[0], options, 'lease on hold') do |vn|
            ar = options[:address_range] || -1

            vn.hold(args[1], ar)
        end
    end

    release_desc = <<-EOT.unindent
        Releases a Virtual Network lease on hold
    EOT

    command :release, release_desc, :vnetid, :ip,
            :options => STD_OPTIONS + [OneVNetHelper::AR] do
        helper.perform_action(args[0], options, 'lease released') do |vn|
            ar = options[:address_range] || -1

            vn.release(args[1], ar)
        end
    end

    reserve_desc = <<-EOT.unindent
        Reserve addresses from the Virtual Network. A new virtual network will
        be created to hold the reservation. Optionally the reservation can be
        put on an exisiting VNET, as long as it contains a valid reservation
        from the same VNET
    EOT

    command :reserve, reserve_desc, :vnetid, [:vnetid, nil],
            :options => STD_OPTIONS + [OneVNetHelper::AR,
                                       OneVNetHelper::NAME,
                                       OneVNetHelper::SIZE,
                                       OneVNetHelper::MAC,
                                       OneVNetHelper::IP,
                                       OneVNetHelper::IP6] do
        helper.perform_action(args[0], options, 'reservation made') do |vn|
            size = options[:size] || -1
            name = options[:name] || -1

            addr = nil
            addr = options[:mac] if options[:mac]
            addr = options[:ip] if options[:ip]
            addr = options[:ip6] if options[:ip6]

            if size == -1
                STDERR.puts 'Specify a size (-s size) for the reservation'
                exit(-1)
            end

            if name == -1 && args[1].nil?
                STDERR.puts 'Specify a name (-n name) for the reservation'
                exit(-1)
            end

            res = vn.reserve(name, size, options[:address_range], addr, args[1])

            if !OpenNebula.is_error?(res)
                puts "Reservation VNET ID: #{res}"
            end

            res
        end
    end

    chgrp_desc = <<-EOT.unindent
        Changes the Virtual Network group
    EOT

    command :chgrp, chgrp_desc, [:range, :vnetid_list], :groupid do
        helper.perform_actions(args[0], options, 'Group changed') do |vn|
            vn.chown(-1, args[1].to_i)
        end
    end

    chown_desc = <<-EOT.unindent
        Changes the Virtual Network owner and group
    EOT

    command :chown, chown_desc, [:range, :vnetid_list], :userid,
            [:groupid, nil] do
        args[2].nil? ? gid = -1 : gid = args[2].to_i
        helper.perform_actions(args[0], options, 'Owner/Group changed') do |vn|
            vn.chown(args[1].to_i, gid)
        end
    end

    chmod_desc = <<-EOT.unindent
        Changes the Virtual Network permissions
    EOT

    command :chmod, chmod_desc, [:range, :vnetid_list], :octet do
        helper.perform_actions(args[0], options, 'Permissions changed') do |vn|
            vn.chmod_octet(OpenNebulaHelper.to_octet(args[1]))
        end
    end

    list_desc = <<-EOT.unindent
        Lists Virtual Networks in the pool. #{OneVNetHelper.list_layout_help}
    EOT

    command :list, list_desc, [:filterflag, nil],
            :options => CLIHelper::OPTIONS + OpenNebulaHelper::OPTIONS +
                        [OpenNebulaHelper::DESCRIBE] do
        helper.list_pool(options, false, args[0])
    end

    show_desc = <<-EOT.unindent
        Shows information for the given Virtual Network
    EOT

    command :show, show_desc, :vnetid,
            :options => [OpenNebulaHelper::FORMAT,
                         OpenNebulaHelper::DECRYPT,
                         OneVNetHelper::SHOW_AR] do
        helper.show_resource(args[0], options)
    end

    update_desc = <<-EOT.unindent
        Update the template contents. If a path is not provided the editor will
        be launched to modify the current content.

        Note: Triggers Virtual Machine updates for used leases.
    EOT

    command :update, update_desc, :vnetid, [:file, nil],
            :options => OpenNebulaHelper::APPEND do
        helper.perform_action(args[0], options, 'modified') do |obj|
            if options[:append]
                str = OpenNebulaHelper.append_template(args[0], obj, args[1])
            else
                str = OpenNebulaHelper.update_template(args[0], obj, args[1])
            end

            helper.set_client(options)
            obj = helper.retrieve_resource(obj.id)

            obj.update(str, options[:append])
        end
    end

    update_ar_desc = <<-EOT.unindent
        Update Address Range variables. SIZE, IP, MAC and TYPE cannot be updated
    EOT

    command :updatear, update_ar_desc, :vnetid, :ar_id, [:file, nil],
            :options => OpenNebulaHelper::APPEND do
        helper.update_ar(args[0], args[1], args[2], options)
    end

    rename_desc = <<-EOT.unindent
        Renames the Virtual Network
    EOT

    command :rename, rename_desc, :vnetid, :name do
        helper.perform_action(args[0], options, 'renamed') do |o|
            o.rename(args[1])
        end
    end

    lock_desc = <<-EOT.unindent
        Locks a Virtual Network to prevent certain actions defined by different levels.
        The show action will never be locked.
        Valid states are: All.
        Levels:
        [Use]: locks Admin, Manage and Use actions.
        [Manage]: locks Manage and Use actions.
        [Admin]: locks only Admin actions.
    EOT

    command :lock, lock_desc, [:range, :vnetid_list],
            :options => [USE, MANAGE, ADMIN, ALL] do
        helper.perform_actions(args[0], options, 'VNet locked') do |vnet|
            if !options[:use].nil?
                level = 1
            elsif !options[:manage].nil?
                level = 2
            elsif !options[:admin].nil?
                level = 3
            elsif !options[:all].nil?
                level = 4
            else
                level = 1
            end
            vnet.lock(level)
        end
    end

    unlock_desc = <<-EOT.unindent
        Unlocks a Virtual Network.
        Valid states are: All.
    EOT

    command :unlock, unlock_desc, [:range, :vnetid_list] do
        helper.perform_actions(args[0], options, 'VNet unlocked') do |vnet|
            vnet.unlock
        end
    end

    show_desc = <<-EOT.unindent
        Shows orphans vnets (i.e vnets not referenced in any template).
    EOT

    command :orphans, show_desc do
        puts helper.check_orphans

        return 0
    end

    recover_desc = <<-EOT.unindent
        Recovers a Virtual Network in ERROR state or waiting for a driver operation
        to complete.
        The recovery may be done by failing, succeeding or retrying the current operation.
        YOU NEED TO MANUALLY CHECK THE VN STATUS, to decide if the
        operation was successful or not, or if it can be retried.

        States for success recovers: LOCK_CREATE, LOCK_DELETE, UPDATE_FAILURE state.
        States for failure recovers: LOCK_CREATE, LOCK_DELETE state.
        States for a retry recover: UPDATE_FAILURE
        States for delete: Any but READY
    EOT

    command :recover, recover_desc, [:range, :vnetid_list],
            :options => [SUCCESS, FAILURE, DELETE, RETRY] do
        if !options[:success].nil?
            result = 1
        elsif !options[:failure].nil?
            result = 0
        elsif !options[:delete].nil?
            result = 2
        elsif !options[:retry].nil?
            result = 3
        else
            error_message = <<-EOT.unindent
            Need to specify the result of the pending action.
            \t--success recover the VN by succeeding the missing action.
            \t--failure recover the VN by failing the missing action.
            \t--delete no recover possible, delete the VN.
            EOT

            STDERR.puts error_message
            exit(-1)
        end

        helper.perform_actions(args[0], options, 'recovering') do |vn|
            vn.recover(result)
        end
    end
end
